[AWS CDK超入門] DynamoDB + Lambda + API GatewayでAPIを作ってみた
AWS CDK とは
AWS Cloud Development Kit (AWS CDK)は AWS のリソースを Typescript や Python 等のコードで定義するフレームワークです。コードで定義したリソースは CloudFormation テンプレートに変換され、デプロイされます。
CDK の対応言語は以下の5つです。(2020 年 5 月 現在)
- Typescript
- JavaScript
- Python
- Java
- C#
AWS CDK のメリットとデメリット
CDK を使うメリットとして主に上げられるのは以下です。
- CloudFormation で書くよりもはるかに少量のコードの記述で済む
- 使い慣れたプログラミング言語が使える
- if 文やループなどのプログラミングロジックが使える
- ライブラリとして切り出し、共有できる
- テストが書ける
デメリットとしては以下が挙げられます。
- プログラミングに慣れていない場合の高い学習コスト
作るもの
AWS Lambda + API Gateway + DynamoDB の構成 で DB からデータを取得する API を AWS CDK で作成してデプロイしてみます。
言語は Typescript を使います。
tsc --v Version 3.7.4
インストール
AWS CDK を利用するにはNode.js >= 10.3.0
が必要です。
npm install -g aws-cdk cdk --version 1.32.2
下準備
新しくフォルダを作成します。
mkdir hello-cdk cd hello-cdk
言語を指定して雛形を作成します。
cdk init --language typescript
今回作成するリソースのモジュールもインストールしておきましょう。
$ npm install @aws-cdk/aws-apigateway $ npm install @aws-cdk/aws-lambda $ npm install @aws-cdk/aws-dynamodb
数が多くなると package.json に記述してnpm install
する方が楽かもしれません。
package.json 記述例
"dependencies": { "@aws-cdk/aws-apigateway": "_", "@aws-cdk/aws-dynamodb": "_", "@aws-cdk/aws-lambda": "\*", // more dependencies }
cdk bootstrap
cdk bootstrap
AWS アカウント、リージョン単位で一度だけ実行するコマンドです。 CDKで利用するリソースを置いておく S3 バケットを作成してくれます。
Watch Mode
npm run watch Starting compilation in watch mode... Found 0 errors. Watching for file changes. ...
上記コマンドで Watch モードを起動します。これで Typescript のトランスパイラが随時 Typescript コードを Javascript にコンパイルしてくれます。オンにしておくことでコードを変更するごとにエラーチェックできるので便利ですし、デプロイする際にコンパイルし忘れるのも防げます。
これで準備は完了です。
早速lib/hello-cdk-stack.ts
に リソース定義を記述してゆきます。
Dynamo DB
まずはじめに DynamoDB のテーブルを定義します。
import \* as cdk from "@aws-cdk/core"; import {Table, AttributeType, } as dynamodb from "@aws-cdk/aws-dynamodb"; export class HelloCdkStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); new Table(this, "items", { partitionKey: { name: "itemId", type: AttributeType.STRING, }, tableName: "items", removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code }); } } const app = new cdk.App(); new HelloCdkStack(app, "HelloCdkStack"); app.synth();
試しにこの状態で cdk deploy
してみると、以下の結果がかえります。
cdk deploy HelloCdkStack: deploying... HelloCdkStack: creating CloudFormation changeset... 0/3 | 2:24:38 AM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata 0/3 | 2:24:38 AM | CREATE_IN_PROGRESS | AWS::DynamoDB::Table | items (items07D08F4B) 0/3 | 2:24:39 AM | CREATE_IN_PROGRESS | AWS::DynamoDB::Table | items (items07D08F4B) Resource creation Initiated 0/3 | 2:24:39 AM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated 1/3 | 2:24:39 AM | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata 2/3 | 2:25:09 AM | CREATE_COMPLETE | AWS::DynamoDB::Table | items (items07D08F4B) 3/3 | 2:25:11 AM | CREATE_COMPLETE | AWS::CloudFormation::Stack | HelloCdkStack
マネジメントコンソールを覗くとitems
テーブルが作成されています。
DynamoDB removalPolicy について
DynamoDB テーブルにデータが入っている状態でスタックを削除しようとした場合、以下のオプションで挙動が変わります。
- RETAIN: テーブルにデータが入っている状態で
cdk destroy
が実行された時、テーブルを削除しない - DESTROY: テーブルにデータが入っている状態で
cdk destroy
が実行された時、データごとテーブルを削除する
テーブル内のデータを意図せずに削除することがないよう本番環境では removalPolicy をRETAIN
に設定することが推奨されています。
Lambda
次に DynamoDB からデータを取得する Lambda Function を定義します。
Lambda のソースコードは/lambda/
というフォルダを新規に作成し、そこに記述することにします。
get-item.ts
に DB から Item を一つ取得する処理を記述します。
const AWS = require("aws-sdk"); const db = new AWS.DynamoDB.DocumentClient(); const TABLE_NAME = process.env.TABLE_NAME || ""; const PRIMARY_KEY = process.env.PRIMARY_KEY || ""; export const handler = async (event: any = {}): Promise<any> => { const requestedItemId = event.pathParameters.id; if (!requestedItemId) { return { statusCode: 400, body: `Error: You are missing the path parameter id`, }; } const params = { TableName: TABLE_NAME, Key: { [PRIMARY_KEY]: requestedItemId, }, }; try { const response = await db.get(params).promise(); return { statusCode: 200, body: JSON.stringify(response.Item) }; } catch (dbError) { return { statusCode: 500, body: JSON.stringify(dbError) }; } };
lib/hello-cdk-stack.ts
に Lambda を追加します。
Lambda から DynamoDB へアクセスするため、IAM Role も作成します。
import * as cdk from "@aws-cdk/core"; import { Table, AttributeType } from "@aws-cdk/aws-dynamodb"; import { AssetCode, Function, Runtime } from "@aws-cdk/aws-lambda"; export class HelloCdkStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const dynamoTable = new Table(this, "items", { partitionKey: { name: "itemId", type: AttributeType.STRING, }, tableName: "items", removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code }); const getItemLambda = new Function(this, "getOneItemFunction", { code: new AssetCode("lib/lambda"), handler: "get-item.handler", runtime: Runtime.NODEJS_10_X, environment: { TABLE_NAME: dynamoTable.tableName, PRIMARY_KEY: "itemId", }, }); // dynamodb読み取り権限をLambdaに付与 dynamoTable.grantReadData(getItemLambda); const api = new RestApi(this, "itemsApi", { restApiName: "Items Service", }); } } const app = new cdk.App(); new HelloCdkStack(app, "HelloCdkStack"); app.synth();
cdk diff
ここまで記述した時点でcdk diff
を実行してみると、既存の Stack との差分をみてくれます。
Lambda、Lambda ServiceRole が表示されることをデプロイする前に一度確認してみましょう。
API Gateway
最後に API Gateway を定義します。
import \* as cdk from "@aws-cdk/core"; import { Table, AttributeType } from "@aws-cdk/aws-dynamodb"; import { AssetCode, Function, Runtime } from "@aws-cdk/aws-lambda"; import { RestApi, LambdaIntegration, IResource, MockIntegration, PassthroughBehavior, } from "@aws-cdk/aws-apigateway"; export class HelloCdkStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const dynamoTable = new Table(this, "items", { partitionKey: { name: "itemId", type: AttributeType.STRING, }, tableName: "items", removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code }); const getItemLambda = new Function(this, "getOneItemFunction", { code: new AssetCode("lib/lambda"), handler: "get-item.handler", runtime: Runtime.NODEJS_10_X, environment: { TABLE_NAME: dynamoTable.tableName, PRIMARY_KEY: "itemId", }, }); // dynamodb読み取り権限をLambdaに付与 dynamoTable.grantReadData(getItemLambda); // ApiGateway const api = new RestApi(this, "sampleApi", { restApiName: "Sample API", }); const items = api.root.addResource("items"); const singleItem = items.addResource("{id}"); const getItemIntegration = new LambdaIntegration(getItemLambda); singleItem.addMethod("GET", getItemIntegration); addCorsOptions(items); } } export function addCorsOptions(apiResource: IResource) { apiResource.addMethod( "OPTIONS", new MockIntegration({ integrationResponses: [ { statusCode: "200", responseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'*'", "method.response.header.Access-Control-Allow-Credentials": "'false'", "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE'", }, }, ], passthroughBehavior: PassthroughBehavior.NEVER, requestTemplates: { "application/json": '{"statusCode": 200}', }, }), { methodResponses: [ { statusCode: "200", responseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Methods": true, "method.response.header.Access-Control-Allow-Credentials": true, "method.response.header.Access-Control-Allow-Origin": true, }, }, ], } ); } const app = new cdk.App(); new HelloCdkStack(app, "HelloCdkStack"); app.synth();
API Gateway CORS(Cross Origin Resource Sharing)対応
API Gatewayに対して CORS を有効にするには、以下の2点を設定する必要があります。
- Lambda のレスポンスに
headers: {"Access-Control-Allow-Origin": "<アクセスを許可したいオリジン>"}
を含める必要がある。 - Options メソッドを追加する
デプロイ&動作確認
お疲れ様でした。ここまで記述ができたらもう一度cdk deploy
を実行してリソースを作成しましょう。
Stackが作成されたことを確認できたら動作をみてみましょう。
DynamoDB へ適当なデータを入れて APIGateway へリクエストを投げてみます。
curl -v https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/items/123 * Trying xx.xxx.xxx.xx... ... < HTTP/2 200 < content-type: application/json < content-length: 39 < date: Tue, 26 May 2020 12:00:31 GMT < x-amzn-requestid: daab105f-f66e-44c6-a4ec-9cc9d80d8bd9 < x-amz-apigw-id: NI2y7GssNjMFpBw= < x-amzn-trace-id: Root=1-5ecd04df-c068ca1ce325b22cc88a43f6;Sampled=0 < x-cache: Miss from cloudfront < via: 1.1 xxxxxxxxx.cloudfront.net (CloudFront) < x-amz-cf-pop: NRT51-C1 < x-amz-cf-id: nahVrJ04K6zfUzuoXTQcBhH6ywmhjTlyLdKw6cAyUczEW8nG_VfpcQ== < * Connection #0 to host xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com left intact {"itemId":"123","itemName":"すあま"}
テンプレート全文
ここまで記述したCDKテンプレートはこちらです。
import * as cdk from "@aws-cdk/core"; import { Table, AttributeType } from "@aws-cdk/aws-dynamodb"; import { AssetCode, Function, Runtime } from "@aws-cdk/aws-lambda"; import { RestApi, LambdaIntegration, IResource, MockIntegration, PassthroughBehavior, } from "@aws-cdk/aws-apigateway"; export class HelloCdkStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // DynamoDB定義 const dynamoTable = new Table(this, "items", { partitionKey: { name: "itemId", type: AttributeType.STRING, }, tableName: "items", removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code }); // Lambda 関数定義 const getItemLambda = new Function(this, "getOneItemFunction", { code: new AssetCode("lib/lambda"), handler: "get-item.handler", runtime: Runtime.NODEJS_10_X, environment: { TABLE_NAME: dynamoTable.tableName, PRIMARY_KEY: "itemId", }, }); // dynamodb読み取り権限をLambdaに付与 dynamoTable.grantReadData(getItemLambda); // ApiGateway const api = new RestApi(this, "sampleApi", { restApiName: "Sample API", }); const items = api.root.addResource("items"); const singleItem = items.addResource("{id}"); const getItemIntegration = new LambdaIntegration(getItemLambda); singleItem.addMethod("GET", getItemIntegration); addCorsOptions(items); } } // Options Method を作成 export function addCorsOptions(apiResource: IResource) { apiResource.addMethod( "OPTIONS", new MockIntegration({ integrationResponses: [ { statusCode: "200", responseParameters: { "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", "method.response.header.Access-Control-Allow-Origin": "'*'", "method.response.header.Access-Control-Allow-Credentials": "'false'", "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE'", }, }, ], passthroughBehavior: PassthroughBehavior.NEVER, requestTemplates: { "application/json": '{"statusCode": 200}', }, }), { methodResponses: [ { statusCode: "200", responseParameters: { "method.response.header.Access-Control-Allow-Headers": true, "method.response.header.Access-Control-Allow-Methods": true, "method.response.header.Access-Control-Allow-Credentials": true, "method.response.header.Access-Control-Allow-Origin": true, }, }, ], } ); } const app = new cdk.App(); new HelloCdkStack(app, "HelloCdkStack"); app.synth();
後片付け
不要なリソースは忘れずに削除しましょう。
cdk destroy Are you sure you want to delete: HelloCdkStack (y/n)? y HelloCdkStack: destroying... ✅ HelloCdkStack: destroyed
CDK コマンド早見表
最後にCDKで実行できるコマンドの一覧をおさらいしてみましょう。
cdk init --language <LANGUAGE> |
初期化 |
cdk bootstrap |
最初に一回だけ実行するおまじない |
cdk ls |
Stack のリストを表示する |
cdk diff |
デプロイ済の Stack との差異を表示する |
cdk synth |
生成された Cfn テンプレートを出力する |
cdk deploy |
Stack を AWS 環境へデプロイする |
cdk destroy <STACK NAME> |
Stack を破棄する |